/**
 * Base64 - Base 64 Transfer Encoding
 *
 * @author Chris Pratt
 *
 * 3/15/1999
 */
package com.anodyzed.onyx.util;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;

public class Base64 {
  public static final char[] STD_DIGIT = {'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/'};
  public static final char[] URL_DIGIT = {'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','0','1','2','3','4','5','6','7','8','9','-','_'};

  private static final long[] ORD = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                                     0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,62, 0,62, 0,63,52,53,54,55,56,57,58,59,60,61, 0, 0, 0, 0, 0, 0,
                                     0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25, 0, 0, 0, 0,63,
                                     0,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51, 0, 0, 0, 0, 0};
  private static final byte[] CRLFb = {(byte)0x0D,(byte)0x0A};
  private static final char[] CRLFc = {(char)0x0D,(char)0x0A};

    /**
     * Constructor
     */
  private Base64 () {
  } //Base64

  /**
   * Encode a character array in base64 mode
   *
   * @param buf the bytes to be encoded
   * @param digits the array of encoding digits
   * @return the encoded string
   */
  private static char[] encode (char[] buf,char[] digits) {
    int b = 0,i = 0,j,s;
    long n;
    int extra = buf.length % 3;
    int top = buf.length - extra;
    int size = ((buf.length / 3) + ((extra == 0) ? 0 : 1)) * 4;
    int pad = 3 - ((buf.length * 4) % 3);
    char[] bytes = new char[size];
    while(i < top) {
      n = ((((long)buf[i++]) & 0xFF) << 16) | ((((long)buf[i++]) & 0xFF) << 8) | (((long)buf[i++]) & 0xFF);
      bytes[b++] = digits[(int)((n >>> 18) & 0x3F)];
      bytes[b++] = digits[(int)((n >>> 12) & 0x3F)];
      bytes[b++] = digits[(int)((n >>> 6) & 0x3F)];
      bytes[b++] = digits[(int)(n & 0x3F)];
    }
    if(extra > 0) {
      n = 0;
      s = 16;
      for(j = 0;j < extra;j++) {
        n |= (((long)buf[i++]) & 0xFF) << s;
        s >>= 1;
      }
      s = 18;
      for(j = 0;j < 4 - pad;j++) {
        bytes[b++] = digits[(int)((n >>> s) & 0x3F)];
        s -= 6;
      }
      for(j = 0;j < pad;j++) {
        bytes[b++] = '=';
      }
    }
    return bytes;
  } //encode

  /**
   * Encode a character array in base64 mode
   *
   * @param bytes the bytes to be encoded
   * @return the encoded string
   */
  public static char[] encode (char[] bytes) {
    return encode(bytes,STD_DIGIT);
  } //encode

  /**
   * Encode a character array in modified base64 mode.
   *
   * This mode replaces the '+' and '/' characters that are problematic in
   * URLs with '-' and '_', respectively.
   *
   * @param bytes the bytes to be encoded
   * @return the encoded string
   */
  public static char[] encodeURLSafe (char[] bytes) {
    return encode(bytes,URL_DIGIT);
  } //encodeURLSafe

  /**
   * Encode an array of bytes
   *
   * @param buf the bytes to be encoded
   * @param ofs The Offset to start encoding
   * @param len The Number of characters to encode
   * @param digits the array of encoding digits
   * @return the encoded string
   */
  private static String encode (byte[] buf,int ofs,int len,char[] digits) {
    int b = 0,i = ofs,j,s;
    long n;
    int extra = len % 3;
    int top = len - extra;
    int size = ((len / 3) + ((extra == 0) ? 0 : 1)) * 4;
    int pad = 3 - ((len * 4) % 3);
    char[] bytes = new char[size];
    while(i < top) {
      n = ((((long)buf[i++]) & 0xFF) << 16) | ((((long)buf[i++]) & 0xFF) << 8) | (((long)buf[i++]) & 0xFF);
      bytes[b++] = digits[(int)((n >>> 18) & 0x3F)];
      bytes[b++] = digits[(int)((n >>> 12) & 0x3F)];
      bytes[b++] = digits[(int)((n >>> 6) & 0x3F)];
      bytes[b++] = digits[(int)(n & 0x3F)];
    }
    if(extra > 0) {
      n = 0;
      s = 16;
      for(j = 0;j < extra;j++) {
        n |= (((long)buf[i++]) & 0xFF) << s;
        s >>= 1;
      }
      s = 18;
      for(j = 0;j < 4 - pad;j++) {
        bytes[b++] = digits[(int)((n >>> s) & 0x3F)];
        s -= 6;
      }
      for(j = 0;j < pad;j++) {
        bytes[b++] = '=';
      }
    }
    return new String(bytes);
  } //encode

  /**
   * Encode an array of bytes
   *
   * @param buf the bytes to be encoded
   * @param ofs The Offset to start encoding
   * @param len The Number of characters to encode
   * @return the encoded string
   */
  public static String encode (byte[] buf,int ofs,int len) {
    return encode(buf,ofs,len,STD_DIGIT);
  } //encode

  /**
   * Encode an array of bytes using modified Base64 encoding.
   *
   * This mode replaces the '+' and '/' characters that are problematic in
   * URLs with '-' and '_', respectively.
   *
   * @param buf the bytes to be encoded
   * @param ofs The Offset to start encoding
   * @param len The Number of characters to encode
   * @return the encoded string
   */
  public static String encodeURLSafe (byte[] buf,int ofs,int len) {
    return encode(buf,ofs,len,URL_DIGIT);
  } //encodeURLSafe

  /**
    * Encode an array of bytes
    *
    * @param buf the bytes to be encoded
    * @param ofs the offset to start encoding
    * @return the encoded string
    */
  public static String encode (byte[] buf,int ofs) {
    return encode(buf,ofs,buf.length - ofs,STD_DIGIT);
  } //encode

  /**
   * Encode an array of bytes using modified Base64 encoding.
   *
   * This mode replaces the '+' and '/' characters that are problematic in
   * URLs with '-' and '_', respectively.
   *
   * @param buf the bytes to be encoded
   * @param ofs the offset to start encoding
   * @return the encoded string
   */
  public static String encodeURLSafe (byte[] buf,int ofs) {
    return encode(buf,ofs,buf.length - ofs,URL_DIGIT);
  } //encodeURLSafe

  /**
   * Encode an array of bytes base64 mode
   *
   * @param buf the bytes to be encoded
   * @return the encoded string
   */
  public static String encode (byte[] buf) {
    return encode(buf,0,buf.length,STD_DIGIT);
  } //encode

  /**
   * Encode an array of bytes using modified Base64 encoding.
   *
   * This mode replaces the '+' and '/' characters that are problematic in
   * URLs with '-' and '_', respectively.
   *
   * @param buf the bytes to be encoded
   * @return the encoded string
   */
  public static String encodeURLSafe (byte[] buf) {
    return encode(buf,0,buf.length,URL_DIGIT);
  } //encodeURLSafe

  /**
   * Push the bytes in the buffer to the output stream
   *
   * @param out The Output Stream to write to
   * @param buf The Bytes to write
   * @param line The Maximum Line Length
   * @param pos The Array of Position Information
   * @throws IOException If the streams get crossed
   */
  private static void push (OutputStream out,byte[] buf,int line,int[] pos) throws IOException {
    if((line > 0) && ((pos[1] + buf.length) > line)) {
      out.write(CRLFb);
      pos[1] = 0;
    }
    out.write(buf);
    pos[0] += buf.length;
    pos[1] += buf.length;
  } //push

  /**
   * Encode the input stream and write it to the output stream, breaking each
   *  line every <tt>line</tt> characters
   *
   * @param in The Input Stream
   * @param out The Output Stream
   * @param line The maximum number of bytes per line
   * @param digit The array of encoding digits
   * @return number of bytes written to <tt>out</tt>
   * @throws IOException If the streams get crossed
   */
  public static int encode (InputStream in,OutputStream out,int line,char[] digit) throws IOException {
    long n;
    int read;
    int[] pos = {0,0};
    byte[] ibuf = new byte[3];
    byte[] obuf = new byte[4];
    if(!(in instanceof BufferedInputStream)) {
      in = new BufferedInputStream(in);
    }
    if(!(out instanceof BufferedOutputStream)) {
      out = new BufferedOutputStream(out);
    }
    while((read = in.read(ibuf)) == 3) {
      n = ((((long)ibuf[0]) & 0xFF) << 16) | ((((long)ibuf[1]) & 0xFF) << 8) | (((long)ibuf[2]) & 0xFF);
      obuf[0] = (byte)digit[(int)((n >>> 18) & 0x3F)];
      obuf[1] = (byte)digit[(int)((n >>> 12) & 0x3F)];
      obuf[2] = (byte)digit[(int)((n >>> 6) & 0x3F)];
      obuf[3] = (byte)digit[(int)(n & 0x3F)];
      push(out,obuf,line,pos);
    }
    if(read > 0) {
      n = (((long)ibuf[0]) & 0xFF) << 16;
      if(read == 2) {
        n |= (((long)ibuf[1]) & 0xFF) << 8;
      }
      obuf[0] = (byte)digit[(int)((n >>> 18) & 0x3F)];
      obuf[1] = (byte)digit[(int)((n >>> 12) & 0x3F)];
      obuf[2] = (byte)((read == 2) ? digit[(int)((n >>> 6) & 0x3F)] : '=');
      obuf[3] = '=';
      push(out,obuf,line,pos);
    }
    out.flush();
    return pos[0];
  } //encode

  /**
   * Encode the input stream and write it to the output stream, breaking each
   *  line every <tt>line</tt> characters
   *
   * @param in The Input Stream
   * @param out The Output Stream
   * @param line The maximum number of bytes per line
   * @return number of bytes written to <tt>out</tt>
   * @throws IOException If the streams get crossed
   */
  public static int encode (InputStream in,OutputStream out,int line) throws IOException {
    return encode(in,out,line,STD_DIGIT);
  } //encode

  /**
   * Encode the input stream and write it to the output stream, breaking each
   *  line every <tt>line</tt> characters
   *
   * @param in The Input Stream
   * @param out The Output Stream
   * @param line The maximum number of bytes per line
   * @return number of bytes written to <tt>out</tt>
   * @throws IOException If the streams get crossed
   */
  public static int encodeURLSafe (InputStream in,OutputStream out,int line) throws IOException {
    return encode(in,out,line,URL_DIGIT);
  } //encodeURLSafe

  /**
   * Encode the input stream and write it to the output stream, breaking each
   *  line at 76 characters
   *
   * @param in The Input Stream
   * @param out The Output Stream
   * @return the number of bytes written
   * @throws IOException if the streams get crossed
   */
  public static int encode (InputStream in,OutputStream out) throws IOException {
    return encode(in,out,76,STD_DIGIT);
  } //encode

  /**
   * Encode the input stream and write it to the output stream, breaking each
   *  line at 76 characters
   *
   * @param in The Input Stream
   * @param out The Output Stream
   * @return the number of bytes written
   * @throws IOException if the streams get crossed
   */
  public static int encodeURLSafe (InputStream in,OutputStream out) throws IOException {
    return encode(in,out,76,URL_DIGIT);
  } //encodeURLSafe

  /**
   * Push the bytes in the buffer to the writer
   *
   * @param out The Writer to write to
   * @param buf The Chars to write
   * @param line The Maximum Line Length
   * @param pos The Array of Position Information
   * @throws IOException if the streams get crossed
   */
  private static void push (Writer out,char[] buf,int line,int[] pos) throws IOException {
    if((line > 0) && ((pos[1] + buf.length) > line)) {
      out.write(CRLFc);
      pos[1] = 0;
    }
    out.write(buf);
    pos[0] += buf.length;
    pos[0] += buf.length;
  } //push

  /**
   * Encode the input stream and write it to the writer, breaking each line
   *  every <tt>line</tt> characters
   *
   * @param in The Input Stream
   * @param out The Writer
   * @param line The maximum number of bytes per line
   * @param digit The array of encoding digits
   * @return number of bytes written to <tt>out</tt>
   * @throws IOException if the streams get crossed
   */
  public static int encode (InputStream in,Writer out,int line,char[] digit) throws IOException {
    long n;
    int read;
    int[] pos = {0,0};
    byte[] ibuf = new byte[3];
    char[] obuf = new char[4];
    if(!(in instanceof BufferedInputStream)) {
      in = new BufferedInputStream(in);
    }
    if(!(out instanceof BufferedWriter)) {
      out = new BufferedWriter(out);
    }
    while((read = in.read(ibuf)) == 3) {
      n = ((((long)ibuf[0]) & 0xFF) << 16) | ((((long)ibuf[1]) & 0xFF) << 8) | (((long)ibuf[2]) & 0xFF);
      obuf[0] = digit[(int)((n >>> 18) & 0x3F)];
      obuf[1] = digit[(int)((n >>> 12) & 0x3F)];
      obuf[2] = digit[(int)((n >>> 6) & 0x3F)];
      obuf[3] = digit[(int)(n & 0x3F)];
      push(out,obuf,line,pos);
    }
    if(read > 0) {
      n = (((long)ibuf[0]) & 0xFF) << 16;
      if(read == 2) {
        n |= (((long)ibuf[1]) & 0xFF) << 8;
      }
      obuf[0] = digit[(int)((n >>> 18) & 0x3F)];
      obuf[1] = digit[(int)((n >>> 12) & 0x3F)];
      obuf[2] = (read == 2) ? digit[(int)((n >>> 6) & 0x3F)] : '=';
      obuf[3] = '=';
      push(out,obuf,line,pos);
    }
    out.flush();
    return pos[0];
  } //encode

  /**
   * Encode the input stream and write it to the writer, breaking each line
   *  every <tt>line</tt> characters
   *
   * @param in The Input Stream
   * @param out The Writer
   * @param line The maximum number of bytes per line
   * @return number of bytes written to <tt>out</tt>
   * @throws IOException if the streams get crossed
   */
  public static int encode (InputStream in,Writer out,int line) throws IOException {
    return encode(in,out,line,STD_DIGIT);
  } //encode

  /**
   * Encode the input stream and write it to the output stream, breaking each
   *  line at 76 characters
   *
   * @param in The Input Stream
   * @param out The Output Stream Writer
   * @return the number of bytes written
   * @throws IOException If the streams get crossed
   */
  public static int encode (InputStream in,Writer out) throws IOException {
    return encode(in,out,76);
  } //encode

  /**
   * Decode the character array from base64
   *
   * @param data The Base64 array to be decoded
   * @return Decoded character array
   */
  public static char[] decode (char[] data) {
    long n;
    int i = 0,j = 0;
    int len = (data.length * 3) / 4;
    if((data[data.length - 1]) == '=') {
      if((data[data.length - 2]) == '=') {
        --len;
      }
      --len;
    }
    char[] buf = new char[len];
    while(i < data.length) {
      n = (ORD[data[i++]] << 18) | (ORD[data[i++]] << 12) | (ORD[data[i++]] << 6) | ORD[data[i++]];
      buf[j++] = (char)((n >>> 16) & 0xFF);
      if(--len > 0) {
        buf[j++] = (char)((n >>> 8) & 0xFF);
        if(--len > 0) {
          buf[j++] = (char)(n & 0xFF);
          --len;
        }
      }
    }
    return buf;
  } //decode

  /**
   * Decode the Reader to the Output Stream, allows interspersed carriage
   * returns and line-feeds.
   *
   * @param in The Input Stream Reader
   * @param out The Output Stream
   * @return The number of bytes written to the Output Stream
   * @throws IOException If the streams get crossed
   */
  public static int decode (Reader in,OutputStream out) throws IOException {
    long n;
    int i,j,len,count = 0;
    String line;
    byte[] buf;
    char[] data;
    BufferedReader reader = (in instanceof BufferedReader) ? (BufferedReader)in : new BufferedReader(in);
    while((line = reader.readLine()) != null) {
      data = line.toCharArray();
      i = 0;
      j = 0;
      len = (data.length * 3) / 4;
      if((data[data.length - 1]) == '=') {
        if((data[data.length - 2]) == '=') {
          --len;
        }
        --len;
      }
      buf = new byte[len];
      while(i < data.length) {
        n = (ORD[data[i++]] << 18) | (ORD[data[i++]] << 12) | (ORD[data[i++]] << 6) | ORD[data[i++]];
        buf[j++] = (byte)((n >>> 16) & 0xFF);
        if(--len > 0) {
          buf[j++] = (byte)((n >>> 8) & 0xFF);
          if(--len > 0) {
            buf[j++] = (byte)(n & 0xFF);
            --len;
          }
        }
      }
      out.write(buf);
      count += len;
    }
    return count;
  } //decode

  /**
   * Decode the Input Stream to the Output Stream, allows interspersed carriage
   * returns and line-feeds.
   *
   * @param in The Input Stream
   * @param out The Output Stream
   * @return The number of bytes written to the Output Stream
   * @throws IOException if the streams get crossed
   */
  public static int decode (InputStream in,OutputStream out) throws IOException {
    return decode(new InputStreamReader(in),out);
  } //decode

  /**
   * Decode the String from base64, allows interspersed carriage returns and
   *  line-feeds.
   *
   * @param str The Base64 String to be decoded
   * @return Decoded byte array
   */
  public static byte[] decode (String str) {
    try {
      ByteArrayOutputStream out = new ByteArrayOutputStream();
      decode(new BufferedReader(new StringReader(str)),out);
      return out.toByteArray();
    } catch(IOException x) {
      return null;
    }
  } //decode

  /**
   * Print the Usage Message
   */
  private static void usage () {
    System.err.println("Usage: java Base64 <encode | encodeURLSafe | decode | file> <data> [<data> ...]");
  } //usage

  /**
   * Program Entry-point
   *
   * @param args Command Line Arguments
   * @throws IOException if the streams get crossed
   */
  public static void main (String[] args) throws IOException {
    if(args.length >= 2) {
      if(args[0].equalsIgnoreCase("encode")) {
        int i;
        StringBuilder buf = new StringBuilder();
        for(i = 1;i < args.length - 1;i++) {
          buf.append(args[i]).append(' ');
        }
        buf.append(args[i]);
        System.err.println(new String(encode(buf.toString().toCharArray())));
      } else if(args[0].equalsIgnoreCase("encodeURLSafe")) {
        int i;
        StringBuilder buf = new StringBuilder();
        for(i = 1;i < args.length - 1;i++) {
          buf.append(args[i]).append(' ');
        }
        buf.append(args[i]);
        System.err.println(new String(encodeURLSafe(buf.toString().toCharArray())));
      } else if(args[0].equalsIgnoreCase("decode")) {
        System.err.println(Hex.dump(new String(decode(args[1]))));
      } else if(args[0].equalsIgnoreCase("file")) {
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        int cnt = encode(new FileInputStream(args[1]),bytes);
        String encoded = new String(bytes.toByteArray());
        System.err.println("Encoded: (" + cnt + ')');
        System.err.println(encoded);
        System.err.println("Decoded:");
        System.err.println(new String(decode(encoded)));
      } else {
        usage();
      }
    } else {
      usage();
    }
  } //main

} //*Base64
